Skip to content

sqlmap源码解析(六)sqlmap是如何检测注入的

字数
1853 字
阅读时间
9 分钟
更新日期
7/28/2018

前面五章一直都没有分析到主要的地方,今天有空,就写下sqlmap是如何检测注入,内在的检测逻辑是怎样的。

之前的分析也有说过,sqlmap整个检测的开始是start()函数,所以一切都要从start()函数开始,在

js
/sqlmap/lib/controller/controller.py
js
 """
    This function calls a function that performs checks on both URL
    stability and all GET, POST, Cookie and User-Agent parameters to
    check if they are dynamic and SQL injection affected
    这个函数功能是检测两个URL的稳定性然后根据GETPOSTCookie和User-Agent参数
    去检测是否存在sql注入
    """

    # direct 对应 -d 命令,直接接管数据
    if conf.direct:
        initTargetEnv()
        setupTargetEnv()
        action()
        return True

    if conf.url and not any((conf.forms, conf.crawlDepth)):
        # 添加目标
        kb.targets.add((conf.url, conf.method, conf.data, conf.cookie, None))

    # 判断没有目标就会报错
    if conf.configFile and not kb.targets:
        errMsg = "you did not edit the configuration file properly, set "
        errMsg += "the target URL, list of targets or google dork"
        logger.error(errMsg)
        return False

    if kb.targets and len(kb.targets) > 1:
        infoMsg = "sqlmap got a total of %d targets" % len(kb.targets)
        logger.info(infoMsg)

函数比较长,我们挑选重要的分段讲解。上面代码的意思还是检测。后面太长就不列出来了,大概意义就是配置HTTP发包响应的各种参数。

如果用户有输入参数设置的话,会设置各种参数。这里一一忽略

python
# 该函数主要包含3个子功能:
# 1.创建保存目标执行结果的目录和文件
# 2.将get或post发送的数据解析成字典形式,并保存到conf.paramDict中
# 3.读取session文件(如果存在的话),并提起文件中的数据,保存到kb变量中
setupTargetEnv()

if not checkConnection(suppressOutput=conf.forms) or not checkString() or not checkRegexp():
    continue

checkWaf() # 检测waf

if conf.identifyWaf:
    identifyWaf() # 识别waf

检测WAF,后面就测试网页的稳定性(查看参数是否是动态的)

后面根据level等级确定注入的位置

屏幕快照 2018-07-28 下午5.10.59.png

后面又经过了一大段判断代码,来到第一个判断注入的地方。

js
# 简单的检测下数字型注入和一些其他漏洞
check = heuristicCheckSqlInjection(place, parameter)

这个函数翻译为中文为启发性注入检测,函数里面内容就是判断一下数字型注入,例如id=1这个参数会变成id=randINT+id-randINT,用两个页面的差异性来判断是否存在注入,如果两者相似度很高,则可以说明存在注入了。

当然,这个参数里面也会用一些payload 来检测XSS和文件包含漏洞。

接着来到了sqlmap检测的核心地方。

js
 injection = checkSqlInjection(place, parameter, value)

这里是整个程序最核心的检测函数,函数在 /lib/controller/checks.py 这个文件中。

在往后,就是保存信息hash之类的东西了。所以,为了找到sqlmap是如何检测注入的,有必要到checks.py文件中去一探究竟。

python
def checkSqlInjection(place, parameter, value):
    # Store here the details about boundaries and payload used to
    # successfully inject
    # 存储成功用过的boundaries 和 pyaload细节
    injection = InjectionDict()

    # Localized thread data needed for some methods
    # 局部现成数据为后面方法
    threadData = getCurrentThreadData()

    # Favoring non-string specific boundaries in case of digit-like parameter values
    # 在类似digit的参数值时,支持非特定于字符串的边界
    if value.isdigit():
        kb.cache.intBoundaries = kb.cache.intBoundaries or sorted(copy.deepcopy(conf.boundaries), key=lambda boundary: any(_ in (boundary.prefix or "") or _ in (boundary.suffix or "") for _ in ('"', '\'')))
        boundaries = kb.cache.intBoundaries
    elif value.isalpha():
        kb.cache.alphaBoundaries = kb.cache.alphaBoundaries or sorted(copy.deepcopy(conf.boundaries), key=lambda boundary: not any(_ in (boundary.prefix or "") or _ in (boundary.suffix or "") for _ in ('"', '\'')))
        boundaries = kb.cache.alphaBoundaries
    else:
        boundaries = conf.boundaries

    # Set the flag for SQL injection test mode
    kb.testMode = True

    paramType = conf.method if conf.method not in (None, HTTPMETHOD.GET, HTTPMETHOD.POST) else place
    tests = getSortedInjectionTests()
    # paramType是注入的类型,如GET。tests是要测试的列表,包含了每个测试项的名称,这些数据都是和/sqlmap/xml/payloads/目录下每个xml相对应的。

    seenPayload = set()

    kb.data.setdefault("randomInt", str(randomInt(10)))
    kb.data.setdefault("randomStr", str(randomStr(10)))

    while tests:
        test = tests.pop(0)

        try:
            if kb.endDetection:
                break

            if conf.dbms is None:
                # If the DBMS has not yet been fingerprinted (via simple heuristic check
                # or via DBMS-specific payload) and boolean-based blind has been identified
                # then attempt to identify with a simple DBMS specific boolean-based
                # test what the DBMS may be
                # 如果数据库管理系统还没有指纹(通过简单的启发式检查)
                # 或通过dbms特定的有效负载)和基于booleande的盲人已经被识别。
                # 然后尝试使用一个简单的基于sql的数据库管理系统
                # 测试什么是DBMS
                if not injection.dbms and PAYLOAD.TECHNIQUE.BOOLEAN in injection.data:
                    if not Backend.getIdentifiedDbms() and kb.heuristicDbms is None and not kb.droppingRequests:
                        kb.heuristicDbms = heuristicCheckDbms(injection) # 启发式检测DBms

                # If the DBMS has already been fingerprinted (via DBMS-specific
                # error message, simple heuristic check or via DBMS-specific
                # payload), ask the user to limit the tests to the fingerprinted
                # DBMS
                # 这里DBMS已经被识别出来了,询问用户是否测试别的DBS
                if kb.reduceTests is None and not conf.testFilter and (intersect(Backend.getErrorParsedDBMSes(), SUPPORTED_DBMS, True) or kb.heuristicDbms or injection.dbms):
                    msg = "it looks like the back-end DBMS is '%s'. " % (Format.getErrorParsedDBMSes() or kb.heuristicDbms or injection.dbms)
                    msg += "Do you want to skip test payloads specific for other DBMSes? [Y/n]"
                    kb.reduceTests = (Backend.getErrorParsedDBMSes() or [kb.heuristicDbms]) if readInput(msg, default='Y', boolean=True) else []

            # If the DBMS has been fingerprinted (via DBMS-specific error
            # message, via simple heuristic check or via DBMS-specific
            # payload), ask the user to extend the tests to all DBMS-specific,
            # regardless of --level and --risk values provided
            # 如果数据库管理系统已被指纹(通过特定于DBMS的错误)
            # 消息,通过简单的启发式检查或通过特定于dbms
            # ,要求用户将测试扩展到所有特定于dbms的测试,
            # 不考虑所提供的级别和风险值
            if kb.extendTests is None and not conf.testFilter and (conf.level < 5 or conf.risk < 3) and (intersect(Backend.getErrorParsedDBMSes(), SUPPORTED_DBMS, True) or kb.heuristicDbms or injection.dbms):
                msg = "for the remaining tests, do you want to include all tests "
                msg += "for '%s' extending provided " % (Format.getErrorParsedDBMSes() or kb.heuristicDbms or injection.dbms)
                msg += "level (%d)" % conf.level if conf.level < 5 else ""
                msg += " and " if conf.level < 5 and conf.risk < 3 else ""
                msg += "risk (%d)" % conf.risk if conf.risk < 3 else ""
                msg += " values? [Y/n]" if conf.level < 5 and conf.risk < 3 else " value? [Y/n]"
                kb.extendTests = (Backend.getErrorParsedDBMSes() or [kb.heuristicDbms]) if readInput(msg, default='Y', boolean=True) else []

            title = test.title
            kb.testType = stype = test.stype
            clause = test.clause
            unionExtended = False
            trueCode, falseCode = None, None

            if conf.httpCollector is not None:
                conf.httpCollector.setExtendedArguments({
                    "_title": title,
                    "_place": place,
                    "_parameter": parameter,
                })

            if stype == PAYLOAD.TECHNIQUE.UNION:
                # 会判断是不是union注入,这个stype就是payload文件夹下面xml文件中的stype,如果是union,就会进入,然后配置列的数量等。

                configUnion(test.request.char)

                if "[CHAR]" in title:
                    if conf.uChar is None:
                        continue
                    else:
                        title = title.replace("[CHAR]", conf.uChar)

                elif "[RANDNUM]" in title or "(NULL)" in title:
                    title = title.replace("[RANDNUM]", "random number")

                if test.request.columns == "[COLSTART]-[COLSTOP]":
                    if conf.uCols is None:
                        continue
                    else:
                        title = title.replace("[COLSTART]", str(conf.uColsStart))
                        title = title.replace("[COLSTOP]", str(conf.uColsStop))

                elif conf.uCols is not None:
                    debugMsg = "skipping test '%s' because the user " % title
                    debugMsg += "provided custom column range %s" % conf.uCols
                    logger.debug(debugMsg)
                    continue

                match = re.search(r"(\d+)-(\d+)", test.request.columns)
                if match and injection.data:
                    lower, upper = int(match.group(1)), int(match.group(2))
                    for _ in (lower, upper):
                        if _ > 1:
                            __ = 2 * (_ - 1) + 1 if _ == lower else 2 * _
                            unionExtended = True
                            test.request.columns = re.sub(r"\b%d\b" % _, str(__), test.request.columns)
                            title = re.sub(r"\b%d\b" % _, str(__), title)
                            test.title = re.sub(r"\b%d\b" % _, str(__), test.title)

sqlmap会根据下面的漏洞类型来生成payload检测注入。

B: Boolean-based blind SQL injection(布尔型注入)

E: Error-based SQL injection(报错型注入)

U: UNION query SQL injection(可联合查询注入)

S: Stacked queries SQL injection(可多语句查询注入)

T: Time-based blind SQL injection(基于时间延迟注入)

Q: inline_query(内联查询)

到这里,sqlmap的生成payload流程已经清晰了,通过boundaries.xml来确定payload的前缀和后缀,然后通过上述技术从sqlmap/xml/payloads/中

根据level等级获取payload最后组合成sqlmap发包用的payload。

撰写

布局切换

调整 VitePress 的布局样式,以适配不同的阅读习惯和屏幕环境。

全部展开
使侧边栏和内容区域占据整个屏幕的全部宽度。
全部展开,但侧边栏宽度可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
全部展开,且侧边栏和内容区域宽度均可调
侧边栏宽度可调,但内容区域宽度不变,调整后的侧边栏将可以占据整个屏幕的最大宽度。
原始宽度
原始的 VitePress 默认布局宽度

页面最大宽度

调整 VitePress 布局中页面的宽度,以适配不同的阅读习惯和屏幕环境。

调整页面最大宽度
一个可调整的滑块,用于选择和自定义页面最大宽度。

内容最大宽度

调整 VitePress 布局中内容区域的宽度,以适配不同的阅读习惯和屏幕环境。

调整内容最大宽度
一个可调整的滑块,用于选择和自定义内容最大宽度。

聚光灯

支持在正文中高亮当前鼠标悬停的行和元素,以优化阅读和专注困难的用户的阅读体验。

ON开启
开启聚光灯。
OFF关闭
关闭聚光灯。

聚光灯样式

调整聚光灯的样式。

置于底部
在当前鼠标悬停的元素下方添加一个纯色背景以突出显示当前鼠标悬停的位置。
置于侧边
在当前鼠标悬停的元素旁边添加一条固定的纯色线以突出显示当前鼠标悬停的位置。